// Copyright 2021 - 2025 Crunchy Data Solutions, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package runtime_test

import (
	"errors"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"gotest.tools/v3/assert"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/client-go/discovery"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/crunchydata/postgres-operator/internal/controller/runtime"
	"github.com/crunchydata/postgres-operator/internal/testing/require"
)

func TestServerSideApply(t *testing.T) {
	ctx := t.Context()
	config, base := require.Kubernetes2(t)
	require.ParallelCapacity(t, 0)

	ns := require.Namespace(t, base)

	dc, err := discovery.NewDiscoveryClientForConfig(config)
	assert.NilError(t, err)

	server, err := dc.ServerVersion()
	assert.NilError(t, err)

	serverVersion, err := version.ParseGeneric(server.GitVersion)
	assert.NilError(t, err)

	t.Run("ObjectMeta", func(t *testing.T) {
		cc := client.WithFieldOwner(base, t.Name())
		constructor := func() *corev1.ConfigMap {
			var cm corev1.ConfigMap
			cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
			cm.Namespace, cm.Name = ns.Name, "object-meta"
			cm.Data = map[string]string{"key": "value"}
			return &cm
		}

		// Create the object.
		before := constructor()
		assert.NilError(t, cc.Patch(ctx, before, client.Apply))
		assert.Assert(t, before.GetResourceVersion() != "")

		// Allow the Kubernetes API clock to advance.
		time.Sleep(time.Second)

		// client.Apply changes the ResourceVersion inadvertently.
		after := constructor()
		assert.NilError(t, cc.Patch(ctx, after, client.Apply))
		assert.Assert(t, after.GetResourceVersion() != "")

		switch {
		case serverVersion.LessThan(version.MustParseGeneric("1.25.15")):
		case serverVersion.AtLeast(version.MustParseGeneric("1.26")) && serverVersion.LessThan(version.MustParseGeneric("1.26.10")):
		case serverVersion.AtLeast(version.MustParseGeneric("1.27")) && serverVersion.LessThan(version.MustParseGeneric("1.27.7")):

			assert.Assert(t, after.GetResourceVersion() != before.GetResourceVersion(),
				"expected https://issue.k8s.io/116861")

		default:
			assert.Assert(t, after.GetResourceVersion() == before.GetResourceVersion())
		}

		// Our [runtime.Apply] generates the correct apply-patch.
		again := constructor()
		assert.NilError(t, runtime.Apply(ctx, cc, again))
		assert.Assert(t, again.GetResourceVersion() != "")
		assert.Assert(t, again.GetResourceVersion() == after.GetResourceVersion(),
			"expected to correctly no-op")
	})

	t.Run("ControllerReference", func(t *testing.T) {
		cc := client.WithFieldOwner(base, t.Name())

		// Setup two possible controllers.
		controller1 := new(corev1.ConfigMap)
		controller1.Namespace, controller1.Name = ns.Name, "controller1"
		assert.NilError(t, cc.Create(ctx, controller1))

		controller2 := new(corev1.ConfigMap)
		controller2.Namespace, controller2.Name = ns.Name, "controller2"
		assert.NilError(t, cc.Create(ctx, controller2))

		// Create an object that is controlled.
		controlled := new(corev1.ConfigMap)
		controlled.Namespace, controlled.Name = ns.Name, "controlled"
		assert.NilError(t,
			controllerutil.SetControllerReference(controller1, controlled, cc.Scheme()))
		assert.NilError(t, cc.Create(ctx, controlled))

		original := metav1.GetControllerOfNoCopy(controlled)
		assert.Assert(t, original != nil)

		// Try to change the controller using client.Apply.
		applied := new(corev1.ConfigMap)
		applied.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
		applied.Namespace, applied.Name = controlled.Namespace, controlled.Name
		assert.NilError(t,
			controllerutil.SetControllerReference(controller2, applied, cc.Scheme()))

		err1 := cc.Patch(ctx, applied, client.Apply, client.ForceOwnership)

		// Patch not accepted; the ownerReferences field is invalid.
		assert.Assert(t, apierrors.IsInvalid(err1), "got %#v", err1)
		assert.ErrorContains(t, err1, "one reference")

		var status *apierrors.StatusError
		assert.Assert(t, errors.As(err1, &status))
		assert.Assert(t, status.ErrStatus.Details != nil)
		assert.Assert(t, len(status.ErrStatus.Details.Causes) != 0)
		assert.Equal(t, status.ErrStatus.Details.Causes[0].Field, "metadata.ownerReferences")

		// Try to change the controller using our [runtime.Apply].
		err2 := runtime.Apply(ctx, cc, applied)

		// Same result; patch not accepted.
		assert.DeepEqual(t, err1, err2,
			// Message fields contain GoStrings of metav1.OwnerReference, 🤦
			// so ignore pointer addresses therein.
			cmp.FilterPath(func(p cmp.Path) bool {
				return p.Last().String() == ".Message"
			}, cmp.Transformer("", func(s string) string {
				return regexp.MustCompile(`\(0x[^)]+\)`).ReplaceAllString(s, "()")
			})),
		)
	})

	t.Run("ServiceSelector", func(t *testing.T) {
		constructor := func(name string) *corev1.Service {
			var service corev1.Service
			service.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service"))
			service.Namespace, service.Name = ns.Name, name
			service.Spec.Ports = []corev1.ServicePort{{
				Port: 9999, Protocol: corev1.ProtocolTCP,
			}}
			return &service
		}

		for _, tt := range []struct {
			name     string
			selector map[string]string
		}{
			{"zero", nil},
			{"empty", make(map[string]string)},
		} {
			t.Run(tt.name, func(t *testing.T) {
				cc := client.WithFieldOwner(base, t.Name())

				intent := constructor(tt.name + "-selector")
				intent.Spec.Selector = tt.selector

				// Create the Service.
				before := intent.DeepCopy()
				assert.NilError(t,
					cc.Patch(ctx, before, client.Apply, client.ForceOwnership))

				// Something external mucks it up.
				assert.NilError(t,
					cc.Patch(ctx, before,
						client.RawPatch(client.Merge.Type(), []byte(`{"spec":{"selector":{"bad":"v2"}}}`)),
						client.FieldOwner("wrong")))

				// client.Apply cannot correct it.
				after := intent.DeepCopy()
				assert.NilError(t,
					cc.Patch(ctx, after, client.Apply, client.ForceOwnership))

				// Perhaps one of:
				// - https://issue.k8s.io/117447
				// - https://github.com/kubernetes-sigs/structured-merge-diff/issues/259
				assert.Assert(t, len(after.Spec.Selector) != len(intent.Spec.Selector),
					"got %v", after.Spec.Selector)

				// Our [runtime.Apply] corrects it.
				again := intent.DeepCopy()
				assert.NilError(t, runtime.Apply(ctx, cc, again))
				assert.Assert(t,
					equality.Semantic.DeepEqual(again.Spec.Selector, intent.Spec.Selector),
					"\n--- again.Spec.Selector\n+++ intent.Spec.Selector\n%v",
					cmp.Diff(again.Spec.Selector, intent.Spec.Selector))

				var count int
				var managed *metav1.ManagedFieldsEntry
				for i := range again.ManagedFields {
					if again.ManagedFields[i].Manager == t.Name() {
						count++
						managed = &again.ManagedFields[i]
					}
				}

				assert.Equal(t, count, 1, "expected manager once in %v", again.ManagedFields)
				assert.Equal(t, managed.Operation, metav1.ManagedFieldsOperationApply)

				// The selector field is forgotten, however.
				assert.Assert(t, managed.FieldsV1 != nil)
				assert.Assert(t, !strings.Contains(string(managed.FieldsV1.Raw), `"f:selector":{`),
					"expected f:selector to be missing from %s", managed.FieldsV1.Raw)
			})
		}
	})
}
